package org.gwu.seuss.service;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.gwu.seuss.communication.crypto.CryptoUtil;
import org.gwu.seuss.communication.exception.AuthenticationException;
import org.gwu.seuss.communication.exception.AuthorizationException;
import org.gwu.seuss.communication.exception.FileException;
import org.gwu.seuss.communication.exception.UserException;
import org.gwu.seuss.communication.request.Actions;
import org.gwu.seuss.communication.request.ClientRequest;
import org.gwu.seuss.communication.request.Permissions;
import org.gwu.seuss.domain.File;
import org.gwu.seuss.domain.Shared;
import org.gwu.seuss.domain.User;
import org.gwu.seuss.persistence.UsersMapper;
import org.springframework.util.Assert;

public class UserServiceImpl extends ServiceBase implements UserService {

	public UserServiceImpl() {

	}

	public UserServiceImpl(UsersMapper usersMapper, CryptoUtil cryptoUtil) {
		this.usersMapper = usersMapper;
		this.cryptoUtil = cryptoUtil;
	}

	@Override
	public void login(String username, String password)
			throws AuthenticationException {
		Assert.hasText(username, "Username is not provided.");
		Assert.hasText(password, "Password is not provided.");

		User user = this.usersMapper.selectUserByEmail(username);
		if (user == null) {
			throw new AuthenticationException("User does not exist.");
		}
		if (user.isDisabled()) {
			throw new AuthenticationException("User is disabled.");
		}

		String salt = user.getSalt();
		String userHash = user.getPasswordHash();
		if (salt == null || userHash == null) {
			throw new AuthenticationException("User was not created properly.");
		}

		byte[] bSalt = Base64.decodeBase64(salt);

		try {
			String proposedHash = cryptoUtil.getPasswordHash(password, bSalt);
			if (!userHash.equals(proposedHash)) {
				throw new AuthenticationException("Password did not match.");
			}
		} catch (GeneralSecurityException e) {
			throw new AuthenticationException("Failed to hash password.");
		}
	}

	@Override
	public User selectUserByEmail(String email) {
		return this.usersMapper.selectUserByEmail(email);
	}

	@Override
	public int updateUsersSessionKey(byte[] sessionKey, String email) {
		return this.usersMapper.updateUsersSessionKey(sessionKey, email);
	}

	@Override
	public int createUser(String username, String password)
			throws GeneralSecurityException, IOException {
		Assert.hasText(password, "Password is not provided.");
		Assert.hasText(username, "Username is not provided.");

		byte[] bSalt = cryptoUtil.generateSalt();
		String passwordHash = cryptoUtil.getPasswordHash(password, bSalt);

		User user = new User();
		user.setEmail(username);
		user.setPasswordHash(passwordHash);
		user.setDisabled(false);
		user.setSalt(Base64.encodeBase64String(bSalt));

		return this.usersMapper.insertUser(user);
	}

	@Override
	public int resetPassword(String username, String newPassword,
			String oldPassword) throws AuthenticationException,
			GeneralSecurityException, IOException {
		Assert.hasText(username, "Username is not provided.");
		Assert.hasText(newPassword, "New password is not provided.");

		byte[] bSalt = cryptoUtil.generateSalt();
		String newPasswordHash = cryptoUtil.getPasswordHash(newPassword, bSalt);

		return this.usersMapper.updateUserPassword(username, newPasswordHash,
				Base64.encodeBase64String(bSalt));
	}

	@Override
	public int disableUser(String username) {
		Assert.hasText(username, "Username is not provided.");

		return this.usersMapper.disableUser(username);
	}

	@Override
	public int enableUser(String username) {
		Assert.hasText(username, "Username is not provided.");

		return this.usersMapper.enableUser(username);
	}

	@Override
	public int updateUserEmail(String oldEmail, String newEmail) {

		Assert.hasText(oldEmail, "Old email address is not provided.");
		Assert.hasText(newEmail, "New email address is not provided.");

		return this.usersMapper.updateUserEmail(oldEmail, newEmail);
	}

	@Override
	public int removeUser(String username) {
		Assert.hasText(username, "Username is not provided.");
		// TODO: remove foreign keys?

		return this.usersMapper.removeUser(username);
	}

	@Override
	public void authorizeAdmin(String username) throws AuthorizationException {
		User user = this.selectUserByEmail(username);
		if (!user.isAdmin()) {
			throw new AuthorizationException("User " + user.getUserId()
					+ " is not authorized to complete requested action");
		}
	}

	@Override
	public void authorizeUser(ClientRequest req) throws AuthorizationException,
			UserException, FileException {

		User user = this.selectUserByEmail(req.getUsername());
		boolean authorized = false;
		
		if (user == null) {
			throw new UserException("No user found with username: "
					+ req.getUsername());
		}
		if (req.getAction().equals(Actions.UPLOAD) && req.getFileId() == -1) {
			authorized = true;
		} else {

			File file = this.filesMapper.selectFileById(req.getFileId());

			if (file == null) {
				throw new FileException("File does not exist");
			}

			authorized = file.getOwnerId() == user.getUserId()
					|| authorizeAction(user, file, req);
		}

		if (!authorized) {
			throw new AuthorizationException("User " + req.getUsername()
					+ " is not authorized to complete requested action");
		}
	}

	private boolean authorizeAction(User user, File file, ClientRequest req)
			throws UserException {

		if (req.getAction().equals(Actions.UPLOAD)) {
			if (req.getFileId() != -1) {
				return file.getFilename().equals(req.getFileName())
						&& authorizeUser(user, file, Permissions.UPLOAD);
			}
		}

		if (req.getAction().equals(Actions.DOWNLOAD)) {
			return authorizeUser(user, file, Permissions.DOWNLOAD);
		}

		if (req.getAction().equals(Actions.DELETE_FILE)) {
			return authorizeUser(user, file, Permissions.DELETE);
		}

		if (req.getAction().equals(Actions.SHARE)) {
			User sharedUser = this.selectUserByEmail(req.getUser()
					.getUsername());
			if (sharedUser == null) {
				throw new UserException("No user found with username: "
						+ req.getUser().getUsername());
			}
		}
		return false;
	}

	private boolean authorizeUser(User user, File file, Permissions permission) {

		List<Shared> sharedList = this.sharedMapper
				.selectAllSharedByUserIdAndFileId(user.getUserId(),
						file.getFileId());
		for (Shared shared : sharedList) {
			if (permission.getValue() == shared.getPermissionId()) {
				return true;
			}
		}
		return false;
	}
}